
var etherUnits = require("../lib/etherUnits.js");
var BigNumber = require('bignumber.js');
const config = require('../config.json');
const pg = require('../db/db');
const { logger } = require('./logger');

var Web3 = require('web3');
var web3;
var isConnected;

var init = async function () {
    try {
        web3 = new Web3(new Web3.providers.WebsocketProvider(config.providers))
        isConnected = await web3.eth.net.isListening()
    } catch (error) {
        logger.error(error)
    }
}
var writeBlockToDB_sql = async function (result) {
    let sql = `INSERT INTO ethblock(
            difficulty, "extraData", "gasLimit", "gasUsed", hash, "logsBloom", miner, "mixHash", nonce, "number", 
            "parentHash", "receiptsRoot", "sha3Uncles", size, "stateRoot", "timestamp", "totalDifficulty", transactions, "transactionsRoot", uncles)
            VALUES ('${result.difficulty}','${result.extraData}',${result.gasLimit},${result.gasUsed},'${result.hash}','${result.logsBloom}','${result.miner}',
                '${result.mixHash}','${result.nonce}',${result.number},'${result.parentHash}',
                '${result.receiptsRoot}','${result.sha3Uncles}',${result.size},'${result.stateRoot}','${result.timestamp}',
                '${result.totalDifficulty}','{${result.transactions}}','${result.transactionsRoot}','{${result.uncles}}');`;
    await pg.none(sql);;
    logger.info('DB successfully written for block #' + result.number);
}

var getTransactionReceipt_sql = (data) => {
    web3.eth.getTransactionReceipt(data.hash, async (err, sets) => {
        // logger.info('getTransactionReceipt:==>', sets);
        let sql = `INSERT INTO ethtransactions( "transactionHash", "transactionIndex", "blockHash", "blockNumber", "gasUsed", "cumulativeGasUsed", "contractAddress", status,
        "from", gas, "gasPrice", hash, input, nonce, "to", value)
        VALUES ('${sets.transactionHash}',${sets.transactionIndex},'${sets.blockHash}', ${sets.blockNumber},${sets.gasUsed},${sets.cumulativeGasUsed},
        '${sets.contractAddress}','${sets.status}','${data.from}','${data.gas}','${data.gasPrice}','${data.hash}','${data.input}','${data.nonce}','${data.to}','${data.value}');`;
        await pg.none(sql);
        if (sets.logs.length != 0) {
            for (let j in sets.logs) {
                /**####################################################调整logs表结构预留代码###########################################################################*/
                // let sql = `INSERT INTO ethlogs("logIndex", "transactionIndex", "transactionHash", "blockHash", "blockNumber", address, data, topics, type)
                //         VALUES (${sets.logs[j].logIndex}, ${sets.logs[j].transactionIndex}, '${sets.logs[j].transactionHash}', '${sets.logs[j].blockHash}',
                //         ${sets.logs[j].blockNumber},'${sets.logs[j].address}', '${sets.logs[j].data}', '{${sets.logs[j].topics}}', '${sets.logs[j].type}');`;
                // await pg.none(sql);
                /**####################################################调整logs表结构预留代码###########################################################################*/
                if (sets.logs[j].topics.length != 0) {
                    let sql = getTopic(sets.logs[j]);
                    await pg.none(sql);
                }
            }
        }
    })
}
let getTopic = (obj) => {
    let sql_filed = `INSERT INTO ethlogs("logIndex", "transactionIndex", "transactionHash", "blockHash", "blockNumber", address, data,type `
    let sql_value = `VALUES (${obj.logIndex}, ${obj.transactionIndex}, '${obj.transactionHash}', '${obj.blockHash}', ${obj.blockNumber},'${obj.address}', '${obj.data}', '${obj.type}'`;

    obj.topics.forEach((element, index) => {
        sql_filed += `, topics_${index}`;
        sql_value += ` , '${element}' `;
    });
    sql_filed += `) `;
    sql_value += `);`;
    return sql_filed + sql_value;
}

/**
  //Just listen for latest blocks and sync from the start of the app.
**/
var listenBlocks = function (config) {
    if (isConnected) {
        web3.eth.subscribe('newBlockHeaders', async (err, blockData) => {
            if (!err) {
                logger.info('Found new block: ' + blockData.hash);
                web3.eth.getBlock(blockData.number, async (error, result) => {
                    if (!error) {
                        let blockObj = await pg.any(`SELECT * FROM ethblock WHERE ethblock."number" = '${blockData.number}'`);
                        if (blockObj.length == 0) {
                            writeBlockToDB(config, result, true);
                            // await pg.none(wirteBlocks(result));
                            if (result.transactions.length != 0) {
                                // wirteTransactions(result.transactions);
                                writeTransactionsToDB(config, result, true);
                            }
                        }
                    } else {
                        logger.error('subscribe newBlockHeaders：', error);
                    }
                })
            }
        });
    } else {
        logger.error('Error: Web3 connection time out trying to get block  retrying connection now');
        listenBlocks(config);
    }
}
/**
  If full sync is checked this function will start syncing the block chain from lastSynced param see README
**/
var syncChain = function (config, nextBlock) {
    if (isConnected) {
        if (typeof nextBlock === 'undefined') {
            prepareSync(config, function (error, startBlock) {
                if (error) {
                    logger.error('ERROR: error: ' + error);
                    return;
                }
                syncChain(config, startBlock);
            });
            return;
        }

        if (nextBlock == null) {
            logger.info('nextBlock is null');
            return;
        } else if (nextBlock < config.startBlock) {
            writeBlockToDB(config, null, true);
            writeTransactionsToDB(config, null, true);
            logger.info('*** Sync Finsihed ***');
            config.syncAll = false;
            return;
        }

        var count = config.bulkSize;
        while (nextBlock >= config.startBlock && count > 0) {
            web3.eth.getBlock(nextBlock, function (error, blockData) {
                if (error) {
                    logger.warn('Warning: error on getting block with hash/number: ' + nextBlock + ': ' + error);
                } else if (blockData == null) {
                    logger.warn('Warning: null block data received from the block with hash/number: ' + nextBlock);
                } else {
                    writeBlockToDB(config, blockData);
                    writeTransactionsToDB(config, blockData);
                }
            });
            nextBlock--;
            count--;
        }

        setTimeout(function () { syncChain(config, nextBlock); }, 500);
    } else {
        logger.error('Error: Web3 connection time out trying to get block ' + nextBlock + ' retrying connection now');
        syncChain(config, nextBlock);
    }
}
/**
  Write the whole block object to DB
**/
var writeBlockToDB = function (config, blockData, flush) {
    var self = writeBlockToDB;
    if (!self.bulkOps) {
        self.bulkOps = [];
    }
    if (blockData && blockData.number >= 0) {
        self.bulkOps.push(blockData);
        logger.info('\t- block #' + blockData.number + ' inserted.');
    }

    if (flush && self.bulkOps.length > 0 || self.bulkOps.length >= config.bulkSize) {
        var bulk = self.bulkOps;
        self.bulkOps = [];
        if (bulk.length == 0) return;
        writeBlockToDB_sql(blockData)
    }
}
/**
  Break transactions out of blocks and write to DB
**/
var writeTransactionsToDB = async function (config, blockData, flush) {
    var self = writeTransactionsToDB;
    if (!self.bulkOps) {
        self.bulkOps = [];
        self.blocks = 0;
    }
    if (blockData && blockData.transactions.length > 0) {
        for (d in blockData.transactions) {
            var txData = blockData.transactions[d];
            txData.timestamp = blockData.timestamp;
            txData.value = etherUnits.toEther(new BigNumber(txData.value), 'wei');
            self.bulkOps.push(txData);
        }
        logger.info('\t- block #' + blockData.number + ': ' + blockData.transactions.length + ' transactions recorded.');
    }
    self.blocks++;

    if (flush && self.blocks > 0 || self.blocks >= config.bulkSize) {
        var bulk = self.bulkOps;
        self.bulkOps = [];
        self.blocks = 0;
        if (bulk.length == 0) return;

        var len = blockData.transactions == undefined ? 0 : blockData.transactions.length

        for (let i = 0; i < len; i++) {
            var data = await web3.eth.getTransaction(blockData.transactions[i]).catch(error => logger.error('getTransaction error:', error))//,  (err, data) => {
            // console.log('getTransaction:=====>', blockData.transactions[i]);
            let transactionsObj = await pg.any(`SELECT * FROM ethtransactions WHERE ethtransactions."transactionHash" = '${data.hash}'`);
            if (transactionsObj.length == 0) {
                // getTransactionReceipt(data);
                getTransactionReceipt_sql(data)
            }
        }
    }
}
/**
  //check oldest block or starting block then callback
**/
var prepareSync = function (config, callback) {
    var blockNumber = null;
    pg.any(`SELECT min(number) FROM ethblock limit 1`).then(async docs => {
        if (!docs || docs.length < 1 || docs[0].min == null) {
            // not found in db. sync from config.endBlock or 'latest'
            if (isConnected) {
                var currentBlock = await web3.eth.getBlockNumber()
                var latestBlock = config.endBlock || currentBlock || 'latest';
                if (latestBlock === 'latest') {
                    web3.eth.getBlock(latestBlock, function (error, blockData) {
                        if (error) {
                            logger.warn('Warning: error on getting block with hash/number: ' + latestBlock + ': ' + error);
                        } else if (blockData == null) {
                            logger.warn('Warning: null block data received from the block with hash/number: ' + latestBlock);
                        } else {
                            logger.info('Starting block number = ' + blockData.number);
                            blockNumber = blockData.number - 1;
                            callback(null, blockNumber);
                        }
                    });
                } else {
                    logger.info('Starting block number = ' + latestBlock);
                    blockNumber = latestBlock - 1;
                    callback(null, blockNumber);
                }
            } else {
                logger.error('Error: Web3 connection error');
                callback(err, null);
            }
        } else {
            blockNumber = parseInt((docs[0].min == null ? 0 : docs[0].min)) - 1;
            logger.info('Old block found. Starting block number = ' + blockNumber);
            callback(null, blockNumber);
        }
    });
}
/**
  Block Patcher
**/
var runPatcher = function (config, startBlock, endBlock) {
    if (!web3 || !isConnected) {
        logger.info('Error: Web3 is not connected. Retrying connection shortly...');
        setTimeout(function () { runPatcher(config); }, 3000);
        return;
    }

    if (typeof startBlock === 'undefined' || typeof endBlock === 'undefined') {
        // get the last saved block
        pg.any(`SELECT max(number) FROM ethblock limit 1`).then(async docs => {
            if (!docs || !docs.length < 1 || docs[0].max == null) {
                // no blocks found. terminate runPatcher()
                logger.info('No need to patch blocks.');
                return;
            }
            var lastMissingBlock = parseInt(docs[0].max) + 1;
            var currentBlock = await web3.eth.getBlockNumber()
            runPatcher(config, lastMissingBlock, currentBlock - 1);
        });
        return;
    }

    var missingBlocks = endBlock - startBlock + 1;
    if (missingBlocks > 0) {
        logger.info('Patching from #' + startBlock + ' to #' + endBlock);
        var patchBlock = startBlock;
        var count = 0;
        while (count < config.patchBlocks && patchBlock <= endBlock) {
            if (!('quiet' in config && config.quiet === true)) {
                logger.info('Patching Block: ' + patchBlock)
            }
            web3.eth.getBlock(patchBlock, function (error, patchData) {
                if (error) {
                    logger.warn('Warning: error on getting block with hash/number: ' + patchBlock + ': ' + error);
                } else if (patchData == null) {
                    logger.warn('Warning: null block data received from the block with hash/number: ' + patchBlock);
                } else {
                    checkBlockDBExistsThenWrite(config, patchData)
                }
            });
            patchBlock++;
            count++;
        }
        // flush
        writeBlockToDB(config, null, true);
        writeTransactionsToDB(config, null, true);

        setTimeout(function () { runPatcher(config, patchBlock, endBlock); }, 1000);
    } else {
        // flush
        writeBlockToDB(config, null, true);
        writeTransactionsToDB(config, null, true);

        logger.info('*** Block Patching Completed ***');
    }
}
/**
  This will be used for the patcher
**/
var checkBlockDBExistsThenWrite = async function (config, patchData, flush) {
    let b = await pg.any(`SELECT * FROM ethblock WHERE ethblock."number" = '${patchData.number}'`);
    if (!b.length) {
        writeBlockToDB(config, patchData, flush);
        // await pg.none(wirteBlocks(result));
        if (patchData.transactions.length != 0) {
            writeTransactionsToDB(config, patchData, flush);
            // wirteTransactions(result.transactions);
        }
    } else if (!('quiet' in config && config.quiet === true)) {
        logger.info('Block number: ' + patchData.number + ' already exists in DB.');
    }
};

init().then(d => {
    // patch missing blocks
    if (config.patch === true) {
        logger.info('Checking for missing blocks');
        runPatcher(config);
    }

    // Start listening for latest blocks
    listenBlocks(config);

    // Starts full sync when set to true in config
    if (config.syncAll === true) {
        logger.info('Starting Full Sync');
        syncChain(config);
    }
}).catch(error => logger.error('init error:', error))

// getTransanctions by transactionHash
let getTransanctions = async (transactionHash) => {
    try {
        // TODO
        // getTransanctions by address
        return await pg.any(`SELECT * FROM ethtransactions WHERE ethtransactions."transactionHash" = '${transactionHash}'`);
        // success

    } catch (error) {
        logger.error("getTransanctions error===>", error);
    }
}

module.exports = { getTransanctions }